package archer.back.controllers

import java.io.{ ByteArrayInputStream, IOException }
import java.sql.Timestamp
import java.text.{ ParseException, SimpleDateFormat }
import java.util.Date

import archer.back.models.{ NestedTreeNode, NewsInfo }
import utils.{ ArcherHelper, CommonDB }
import io.circe.syntax._
import io.circe.generic.auto._

import scala.concurrent.{ ExecutionContext, Future }
import cats.implicits._
import models.TableData
import org.jsoup.Jsoup
import org.xarcher.emiya.utils.{ FutureLimitedGen, FutureTimeLimitedGen }
import play.api.cache.AsyncCacheApi
import play.api.libs.ws.WSClient

import scala.annotation.tailrec
import scala.util.{ Failure, Success, Try }
import scala.collection.JavaConverters._

class UpdateService(
  commonDB: CommonDB,
  wsClient: WSClient,
  futureTimeLimitedGen: FutureTimeLimitedGen,
  futureLimitedGen: FutureLimitedGen)(implicit ec: ExecutionContext)
  extends ArcherHelper {

  import models.CrawlTables._
  import models.CrawlTables.profile.api._

  def list: Future[NestedTreeNode] = {
    val nodesF = commonDB.db.run(UpdateTreeNode.to[List].result)
    val nestedNodes = nodesF.map(s => NestedTreeNode.fromUpdateTreeNodeRow(s))
    nestedNodes
  }

  def create(parentId: Int): Future[Boolean] = {
    val model = UpdateTreeNodeRow(
      id = -1,
      name = "新节点",
      parentId = parentId,
      modifiedTime =
        new Timestamp(new Date().getTime))
    val action = UpdateTreeNode += model
    commonDB.db.run(action.transactionally).map((_: Int) => true)
  }

  def update(id: Int, name: String): Future[Boolean] = {
    val action = UpdateTreeNode
      .filter(s => s.id === id)
      .map(s => (s.name, s.modifiedTime))
      .update((name, new Timestamp(new Date().getTime)))
    commonDB.db.run(action.transactionally).map((_: Int) => true)
  }

  def delete(id: Int): Future[Boolean] = {
    val action =
      UpdateTreeNode.filter(s => s.parentId === id).result.headOption.flatMap {
        case Some(_) =>
          DBIO.successful(false)
        case _ =>
          UpdateTreeNode.filter(s => s.id === id).delete.map(_ > 0)
      }
    commonDB.db.run(action.transactionally)
  }

  def moveNode(sourceId: Int, parentId: Int): Future[Boolean] = {
    //parentId > 0 表示不为根 node
    val action =
      UpdateTreeNode
        .filter(s => (s.id === sourceId) && (s.parentId > 0))
        .map(_.parentId)
        .update(parentId)
        .map(_ > 0)
    commonDB.db.run(action.transactionally)
  }

  /*def fetchSubs1111(list: List[UpdateTreeNodeRow],
                    parentId: Int): List[UpdateTreeNodeRow] = {
    list.filter(_.id === parentId) ::: list
      .filter(_.parentId === parentId)
      .flatMap(item => fetchSubs(list, item.id))
  }*/

  case class NodeWrap(node: UpdateTreeNodeRow, isParent: Boolean)

  protected def fetchSubs(
    list: List[UpdateTreeNodeRow],
    parentId: Int): List[NodeWrap] = {
    val result = fetchSubsGen(list, List(parentId), List.empty)
    result.map(s =>
      NodeWrap(node = s, isParent = result.map(_.parentId).contains(s.id)))
  }

  @tailrec
  private final def fetchSubsGen(
    list: List[UpdateTreeNodeRow],
    parentIds: List[Int],
    resultList: List[UpdateTreeNodeRow]): List[UpdateTreeNodeRow] = {
    val (confirm, notConfirm) = list.filter(s =>
      parentIds.contains(s.parentId) || parentIds.contains(s.id)) -> list
      .filterNot(s =>
        parentIds.contains(s.parentId) || parentIds.contains(s.id))
    //println((confirm, notConfirm))
    if (confirm.isEmpty) {
      resultList.distinct
    } else {
      fetchSubsGen(
        notConfirm,
        (parentIds ::: confirm.map(_.id)).distinct,
        confirm ::: resultList)
    }
  }

  protected def getCrawlModels(parentId: Int): Future[List[NodeWrap]] = {
    //parentId > 0 表示不为根 node
    val action =
      UpdateTreeNode.to[List].result
    commonDB.db.run(action).map { list =>
      fetchSubs(list, parentId)
    }
  }

  def createCrawlAction(parentId: Int): Future[UpdateCrawlPlanRow] = {
    getCrawlModels(parentId).flatMap { list =>
      val parentNode = list.find(s => s.node.id === parentId).get
      val plan =
        UpdateCrawlPlanRow(
          id = -1,
          name = s"${parentNode.node.name}及其子 Node 抓取计划",
          parentNodeId = parentNode.node.id,
          modifiedTime = new Timestamp(new Date().getTime))
      val action = UpdateCrawlPlan
        .returning(UpdateCrawlPlan.map(_.id))
        .into((model, id) => model.copy(id = id)) += plan

      val updateActionRows = action.map(
        plan =>
          list
            .filter(_.isParent === false)
            .map(
              s =>
                NodeInfo
                  .filter(_.treeNodeId === s.node.id)
                  .result
                  .headOption
                  .map {
                    case Some(r) =>
                      val model = UpdateCrawlActionRow(
                        id = -1,
                        nodeName = s.node.name,
                        url = r.url,
                        delayHours = r.delayHours,
                        titleSelector = r.titleSelector,
                        timeSelector = r.timeSelector,
                        timeFormatter = r.timeFormatter,
                        planId = plan.id,
                        treeNodeId = r.treeNodeId,
                        isSuccessed = Option.empty,
                        newsTitle = Option.empty,
                        updateTime = Option.empty,
                        retryTimes = 0,
                        modifiedTime = new Timestamp(new Date().getTime))
                      List(UpdateCrawlAction += model)
                    case _ => List.empty
                  }) -> plan)
      val actionRowsToinsert =
        updateActionRows.flatMap {
          case (s, plan) =>
            DBIO.sequence(s).flatMap { t =>
              DBIO.sequence(t.flatten).map((_: Seq[Int]) => plan)
            }
        }
      commonDB.db
        .run(actionRowsToinsert.transactionally)
        .andThen {
          case Success(_) =>
            execCrawlAction.andThen {
              case Success(_) =>
              case Failure(e) => e.printStackTrace
            }
          case Failure(e) => e.printStackTrace
        }
    }
  }

  val timePool =
    futureTimeLimitedGen.create(64, "网页抓取时间限流", 1000)
  val occurPool = futureLimitedGen.create(16, "网页抓取并发限流")

  def execCrawlAction: Future[Boolean] = {
    def limitFuture[T](f: () => Future[T]): Future[T] =
      occurPool.limit(() => timePool.limit(() => f(), 1, "发出请求"), 1, "发出请求")
    commonDB.db
      .run(
        UpdateCrawlAction
          .to[List]
          .filter(s =>
            (s.retryTimes <= 3) && ((s.isSuccessed === false) || s.isSuccessed.isEmpty))
          .result)
      .flatMap {
        case actions @ (head :: tail) =>
          val insertModelsF: Seq[Future[UpdateCrawlActionRow]] = actions.map {
            action =>
              Try {
                val formatter = new SimpleDateFormat(action.timeFormatter)
                limitFuture(
                  () =>
                    wsClient
                      .url(action.url)
                      .get)
                  .map { response =>
                    Try {
                      val chars = response.bodyAsBytes
                      val is = new ByteArrayInputStream(chars.toArray)
                      val doc = Jsoup.parse(is, null: String, action.url)
                      val titles = doc
                        .select(action.titleSelector)
                        .asScala
                        .toList
                        .map(_.text)
                      val timeStrs = doc
                        .select(action.timeSelector)
                        .asScala
                        .toList
                        .map(_.text)

                      val infos = titles.zip(timeStrs).map {
                        case (eachTitle, eachTimeStr) =>
                          val date = formatter.parse(eachTimeStr)
                          NewsInfo(title = eachTitle, updateTime = date)
                      }
                      infos match {
                        case head :: tail =>
                          action.copy(
                            isSuccessed = Option(true),
                            newsTitle = Option(head.title),
                            updateTime = Option(new Timestamp(
                              head.updateTime.getTime + (8L * 60L * 60L * 1000L + 1000L))),
                            retryTimes = action.retryTimes + 1)
                        case _ =>
                          action.copy(
                            isSuccessed = Option(true),
                            newsTitle = Option.empty,
                            updateTime = Option.empty,
                            retryTimes = action.retryTimes + 1)
                      }
                    }.recover {
                      case e: ParseException =>
                        action.copy(
                          isSuccessed = Option(false),
                          newsTitle = Option.empty,
                          updateTime = Option.empty,
                          retryTimes = action.retryTimes + 1)
                      case e: IOException =>
                        action.copy(
                          isSuccessed = Option(false),
                          newsTitle = Option.empty,
                          updateTime = Option.empty,
                          retryTimes = action.retryTimes + 1)
                    }
                  }
                  .transform { s =>
                    s.flatten match {
                      case s @ Success(_) =>
                        s
                      case Failure(_) =>
                        Try {
                          action.copy(
                            isSuccessed = Option(false),
                            newsTitle = Option.empty,
                            updateTime = Option.empty,
                            retryTimes = action.retryTimes + 1)
                        }
                    }
                  }
              }.getOrElse(Future.successful(
                action.copy(
                  isSuccessed = Option(false),
                  newsTitle = Option.empty,
                  updateTime = Option.empty,
                  retryTimes = action.retryTimes + 1)))
          }
          val insertActionsF = Future
            .sequence(insertModelsF)
            .flatMap(
              updateModels =>
                commonDB.db.run(DBIO
                  .sequence(updateModels.map(a =>
                    UpdateCrawlAction.filter(_.id === a.id).update(a)))
                  .transactionally))
          insertActionsF.flatMap((_: Seq[Int]) => execCrawlAction)
        case _ =>
          Future.successful(true)
      }
  }

}
